package br.ufcg.edu.compressao;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;

import br.ufcg.edu.algoritimo.Huffman;
import br.ufcg.edu.algoritimo.No;
import br.ufcg.edu.util.Constantes;

public class Compressao implements CompressaoIF {
	private DataInputStream inputStream;
	private DataOutputStream outputStream;
	private int ultimoCaracter;
	private boolean possuiUltimoCaracter;
	
	public Compressao(InputStream inputStream, OutputStream outputStream) {
		this.inputStream = new DataInputStream(new BufferedInputStream(
				inputStream));
		this.outputStream = new DataOutputStream(new BufferedOutputStream(
				outputStream));
	}

	@Override
	public void start() throws IOException {
		Huffman huffman = new Huffman();

		StringBuilder arquivoString = new StringBuilder();;

		arquivoString = conversorArquivoParaString();
		
		Map<Integer, AtomicInteger> caracterOcorrencia = new HashMap<Integer, AtomicInteger>();

		contarCaracteres(arquivoString, caracterOcorrencia);

		List<No> caracteres = criarListaDeCaracteres(caracterOcorrencia);
		
		Map<Character, String> mapeamento = huffman.criarMapeamento(huffman.construirArvore(caracteres));

		try {
			gravarSaida(arquivoString, caracteres, mapeamento);
		} catch (IOException e) {
			System.err.println("Problemas na escrita do arquivo!");
		}
		
	}

	public void contarCaracteres(StringBuilder arquivoString,
			Map<Integer, AtomicInteger> caracterOcorrencia) {
		for (int i = 0; i < arquivoString.length(); i++) {
			adicionarCarater(caracterOcorrencia, arquivoString.charAt(i));
		}
	}

	private void adicionarCarater(Map<Integer, AtomicInteger> caracterOcorrencia,
			Character caracter) {

		int caract = (int) caracter;
		if (!caracterOcorrencia.containsKey(caract)) {
			caracterOcorrencia.put(caract, new AtomicInteger());
		}
		caracterOcorrencia.get(caract).incrementAndGet();
	}

	//TODO teste para verificar essas entradas
	public StringBuilder conversorArquivoParaString() throws IOException {
		StringBuilder retorno = new StringBuilder();
		DataInputStream reader = this.inputStream;
		while (reader.available() >= 2) {
			retorno.append((char) reader.readChar());
		}
		if(reader.available() == 1){
			possuiUltimoCaracter = true;
			ultimoCaracter = reader.read();
		}
		
		return retorno;
	}

	public void gravarSaida(StringBuilder arquivoString, List<No> caracteres,
			Map<Character, String> mapeamento) throws IOException {

		DataOutputStream writer = new DataOutputStream(
				new BufferedOutputStream(this.outputStream));

		salvarCabecalho(writer, caracteres);
		salvarConteudo(writer, arquivoString, mapeamento);
		salvaUltimocaracter(writer);
		writer.close();
	}

	private void salvaUltimocaracter(DataOutputStream writer) throws IOException {
		if(possuiUltimoCaracter){
			writer.write(ultimoCaracter);
		}
		
	}

	private void salvarConteudo(DataOutputStream writer, StringBuilder arquivoString,
			Map<Character, String> mapeamento) throws IOException {
		Queue<String> stringBuilder = new LinkedList<String>();
		
		for (int i = 0; i < arquivoString.length(); i++) {
			stringBuilder.add(mapeamento.get(arquivoString.charAt(i)));
		}

		salvaCargaUtil(stringBuilder, writer);
		
	}

	private void salvaCargaUtil(Queue<String> queue,
			DataOutputStream writer) throws NumberFormatException, IOException {
		
		StringBuilder carga = new StringBuilder();
		while(!queue.isEmpty()){
			carga.append(queue.poll());
			
			while(carga.length() >= Constantes.NUMERO_DE_BITS_DE_CARACTER){
				writer.writeChar(Integer.parseInt(carga.substring(0, Constantes.NUMERO_DE_BITS_DE_CARACTER), 2));
				carga.delete(0, Constantes.NUMERO_DE_BITS_DE_CARACTER);
			}
		}
		
		if(carga.length() > 0){
			completarString(carga);
			while(carga.length() >= Constantes.NUMERO_DE_BITS_DE_CARACTER){
				writer.writeChar(Integer.parseInt(carga.substring(0, Constantes.NUMERO_DE_BITS_DE_CARACTER), 2));
				carga.delete(0, Constantes.NUMERO_DE_BITS_DE_CARACTER);
			}
		}
		
	}

	private static void completarString(StringBuilder carga) {
		int resto = carga.length()
				% Constantes.NUMERO_DE_BITS_DE_CARACTER;
		
		if(resto == 0){
			return;
		}
		
		int numeroCompletar = Constantes.NUMERO_DE_BITS_DE_CARACTER
				- resto;
		
		for (int i = 0; i < numeroCompletar; i++) {
			carga.append(Constantes.BIT_ZERO);
		}

	}

	private void salvarCabecalho(DataOutputStream writer,
			List<No> caracteres) throws IOException {
		
		writer.writeChar((caracteres.size()));
		for (No no : caracteres) {
			Character descricao = no.getDescricao();
			writer.writeChar(descricao.charValue());
			writer.writeInt(no.getValor());
			if (no.getValor() < 0) {
				System.out.println("MENOR QUE ZERO!");
			}
		}
	}

	public List<No> criarListaDeCaracteres(
			Map<Integer, AtomicInteger> caracterOcorrencia) {
		List<No> nos = new ArrayList<No>();

		for (int caracter : caracterOcorrencia.keySet()) {
			Integer ocorrencia = caracterOcorrencia.get(caracter).get();
			nos.add(new No(null, null, ocorrencia, (char) caracter, true));
		}

		if (nos.size() == 1) {
			nos.add(No.NULO);
		}

		return nos;
	}

	public static void main(String[] args) throws IOException {
		Compressao comp = new Compressao(System.in, System.out);
		comp.start();
		
	}
	
}
